package se.cth.hedgehogphoto.geocoding.model;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import se.cth.hedgehogphoto.log.Log;
import se.cth.hedgehogphoto.objects.LocationGPSObject;
/**
* Parses XML-documents from the nominatim-server
* and extracts the essential information about the
* locations. Requests to the server are limited to
* 1 per second, ie a new request can start 1000 ms
* after the last one was processed. This will result
* in a slightly longer waiting-period, depending on
* how fast the XML-documents are processed. This is
* in line with the nominatim usage policy, which
* should be provided with this software.
* @author Florian Minges
*/
public class XMLParser implements Runnable {
private static XMLParser xmlParser;
private DocumentBuilder docBuilder;
private Map<String, List<LocationGPSObject>> cachedSearchResults = new HashMap<String, List<LocationGPSObject>>();
public static synchronized XMLParser getInstance() {
if (xmlParser == null) {
xmlParser = new XMLParser();
}
return xmlParser;
}
private XMLParser() {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
try {
this.docBuilder = docBuilderFactory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
//processSearch can't run if this happens, since docBuilder is null
Log.getLogger().log(Level.SEVERE, "ParserConfigurationException", e);
}
}
public List<LocationGPSObject> processGeocodingSearch(URL xmlFileUrl) {
List<LocationGPSObject> locations = processGeocoding(xmlFileUrl, RequestType.GEOCODING_REQUEST);
return locations;
}
public LocationGPSObject processReverseGeocodingSearch(URL xmlFileUrl) {
List<LocationGPSObject> list = processGeocoding(xmlFileUrl, RequestType.REVERSE_GEOCODING_REQUEST);
return (list == null || list.size() == 0) ? null : list.get(0);
}
/**
* Processes an XML-file requested from the nominatim-server,
* either from the geocoding, or the reverse-geocoding-service.
* Caches the searchresults to minimize the request to the
* nominatim server. This is in line with the nominatim usage
* policy.
* @param xmlFileUrl the URL to the searchResult from the nominatim
* server.
* @param type the kind of request; Geocoding or Reverse-Geocoding.
* @return a List of LocationObjects containing the result of the
* search.
*/
public synchronized List<LocationGPSObject> processGeocoding(URL xmlFileUrl, RequestType type) {
if (xmlFileUrl == null || type == null) {
return new ArrayList<LocationGPSObject>();
}
/* Check for cached search results first. */
if (this.cachedSearchResults.containsKey(xmlFileUrl.toString())) {
return this.cachedSearchResults.get(xmlFileUrl.toString());
}
try {
final String tagName = (type == RequestType.GEOCODING_REQUEST) ? "place" : "result";
Document doc = this.docBuilder.parse(xmlFileUrl.toString());
// normalize text representation
doc.getDocumentElement().normalize();
NodeList listOfPlaces = doc.getElementsByTagName(tagName);
int nbrOfPlaces = listOfPlaces.getLength();
List<LocationGPSObject> locations = new LinkedList<LocationGPSObject>();
for (int index = 0; index < nbrOfPlaces; index++) {
Node placeID = listOfPlaces.item(index);
if (placeID.getNodeType() == Node.ELEMENT_NODE) {
Element placeElement = (Element) placeID;
String lat = placeElement.getAttribute("lat");
String lon = placeElement.getAttribute("lon");
String place = (type == RequestType.GEOCODING_REQUEST) ?
placeElement.getAttribute("display_name") :
placeElement.getTextContent();
LocationGPSObject location = new LocationGPSObject(place);
try {
location.setLongitude(Double.parseDouble(lon));
location.setLatitude(Double.parseDouble(lat));
locations.add(location);
} catch (NumberFormatException nf) { //don't add location
}
}
}
this.cachedSearchResults.put(xmlFileUrl.toString(), locations); //cache results
return locations;
} catch (SAXParseException error) {
//create error-message
StringBuilder sb = new StringBuilder("** Parsing error, line ");
sb.append(error.getLineNumber());
sb.append(", uri ");
sb.append(error.getSystemId());
sb.append(" ");
sb.append(error.getMessage());
Log.getLogger().log(Level.SEVERE, sb.toString(), error);
} catch (SAXException e) {
Exception x = e.getException();
Log.getLogger().log(Level.SEVERE, e.getMessage(), (x == null) ? e : x);
} catch (Throwable t) {
Log.getLogger().log(Level.SEVERE, "Error" , t);
} finally {
Thread t = new Thread(this);
t.start();
try {
t.join(); /*wait for sleep to finish before leaving the lock
*to this query-method*/
} catch (InterruptedException ie) {
Log.getLogger().log(Level.SEVERE, "Thread got interrupted.", ie);
}
}
return null;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.getLogger().log(Level.SEVERE, "InterruptedException", e);
}
}
}